package mutate

import (
	"github.com/jmespath/go-jmespath"
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
	"gitverse.ru/synapse/kubelatte/pkg/logs"
	"gitverse.ru/synapse/kubelatte/pkg/util"
	"sigs.k8s.io/yaml"
	"strings"
)

type detailedRefTemplateObjectsData struct {
	containers  map[string][]string
	annotations map[string][]string
	volumes     map[string][]string
}

func (r *MutateController) getRendersForPod(templs map[string]v1alpha1.Template,
	obj map[string]interface{}, config v1alpha1.MutationConfig) []util.RenderItem {
	var detailedData detailedRefTemplateObjectsData
	var renders []util.RenderItem
	strategy := config.UpdateStrategy

	logs.Debugf("[getRendersForPod] config.Containers: %v, config.Annotations: %v, config.Volumes: %v", config.Containers, config.Annotations, config.Volumes)

	detailedData = prepareDataForUniversalMutation(config.Containers, config.Annotations, config.Volumes)

	renders = append(renders, r.cutAndApplyTemplate(detailedData.containers, templs, obj, "container", strategy)...)
	renders = append(renders, r.cutAndApplyTemplate(detailedData.annotations, templs, obj, "annotation", strategy)...)
	renders = append(renders, r.cutAndApplyTemplate(detailedData.volumes, templs, obj, "volume", strategy)...)

	for i, render := range renders {
		logs.Debugf("render %v: %v\n", i, render)
	}

	return renders
}

func prepareDataForUniversalMutation(containers []string, annotations []string, volumes []string) (attitude detailedRefTemplateObjectsData) {
	attitude = detailedRefTemplateObjectsData{
		containers:  make(map[string][]string),
		annotations: make(map[string][]string),
		volumes:     make(map[string][]string),
	}

	attitude.containers = appendCutElem(attitude.containers, containers)
	attitude.annotations = appendCutElem(attitude.annotations, annotations)
	attitude.volumes = appendCutElem(attitude.volumes, volumes)

	return attitude
}

func appendCutElem(attitude map[string][]string, elems []string) map[string][]string {
	for _, elem := range elems {
		ref := strings.Split(elem, "/")
		cutInfo := ref[0] + "/" + ref[1]

		attitude[cutInfo] = append(attitude[cutInfo], strings.Join(ref[2:], "/"))
	}

	return attitude
}

func (r *MutateController) cutAndApplyTemplate(resource map[string][]string,
	templs map[string]v1alpha1.Template, obj map[string]interface{}, resourceType string, strategy string) []util.RenderItem {
	var renders []util.RenderItem
	var patches []string

	for ref, names := range resource {
		templ, ok := templs[ref]
		if !ok {
			logs.Debugf("There is no refs to %s, continue", ref)
			continue
		}

		rs := r.applyTemplate(templ, obj, string(strategy))
		if len(rs) == 0 {
			logs.Errorf("Render is empty, nothing for mutate")
			continue
		}
		templ.Spec.Data = rs[0].Render

		var specData interface{}
		if err := yaml.Unmarshal([]byte(templ.Spec.Data), &specData); err != nil {
			logs.Errorf("Converting templ.spec.data to yaml error: %v", err)
			continue
		}

		specData = convert(specData)

		for _, name := range names {
			switch resourceType {
			case "container":
				patches = append(patches, getPatchForContainer(specData, name, obj, strategy)...)
			case "volume":
				patches = append(patches, getPatchForVolume(specData, name, obj, strategy)...)
			case "annotation":
				patches = append(patches, getPatchForAnnotation(specData, name)...)
			}
		}

		for _, patch := range patches {
			templ.Spec.Data = patch
			renders = append(renders, r.applyTemplate(templ, obj, string(strategy))...)
		}
	}

	return renders
}

func getPatchForContainer(specData interface{}, contName string, obj map[string]interface{}, strategy string) []string {
	spec, ok := specData.(map[string]interface{})["spec"]
	if !ok {
		return nil
	}
	containers, ok := spec.(map[string]interface{})["containers"]
	if !ok {
		return nil
	}

	var patches []string

	for _, contToInsert := range containers.([]interface{}) {
		res, ok := contToInsert.(map[string]interface{})["name"]
		if !ok || res != contName {
			continue
		}

		if strategy == MergeStrategy {
			patches = append(patches, getPatchForContainerMergeStrategy(contToInsert.(map[string]interface{})))
		}

		if strategy == ReplaceStrategy {
			patches = append(patches, getPatchForContainerReplaceStrategy(contName, obj, contToInsert.(map[string]interface{})))
		}
	}

	return patches
}

func getPatchForContainerMergeStrategy(contToInsert map[string]interface{}) string {
	withFantic := map[string]map[string][]map[string]interface{}{"spec": {"containers": {contToInsert}}}
	yamlPatch, err := yaml.Marshal(withFantic)
	if err != nil {
		return ""
	}
	return string(yamlPatch)
}

func getPatchForContainerReplaceStrategy(contName string, obj, contToInsert map[string]interface{}) string {
	objConts, err := jmespath.Search("spec.containers", obj)
	if err != nil || objConts == nil {
		return ""
	}

	objContainers, ok := objConts.([]interface{})
	if !ok {
		return ""
	}

	var yamlPatch []byte

	for i := range objContainers {
		item := objContainers[i].(map[string]interface{})
		if item["name"] != contName {
			continue
		}

		objContainers[i] = contToInsert
		withFantic := map[string]map[string][]interface{}{"spec": {"containers": objContainers}}
		yamlPatch, err = yaml.Marshal(withFantic)
		if err != nil {
			return ""
		}
	}

	if len(yamlPatch) != 0 {
		return string(yamlPatch)
	}

	objContainers = append(objContainers, contToInsert)

	withFantic := map[string]map[string][]interface{}{"spec": {"containers": objContainers}}
	yamlPatch, err = yaml.Marshal(withFantic)
	if err != nil {
		return ""
	}

	return string(yamlPatch)
}

func getPatchForVolume(specData interface{}, volName string, obj map[string]interface{}, strategy string) []string {
	spec, ok := specData.(map[string]interface{})["spec"]
	if !ok {
		return nil
	}
	volumes, ok := spec.(map[string]interface{})["volumes"]
	if !ok {
		return nil
	}

	var patches []string

	for _, volToInsert := range volumes.([]interface{}) {
		res, ok := volToInsert.(map[string]interface{})["name"]
		if !ok || res != volName {
			continue
		}

		if strategy == MergeStrategy {
			patches = append(patches, getPatchForVolumeMergeStrategy(volToInsert.(map[string]interface{})))
		}
		if strategy == ReplaceStrategy {
			patches = append(patches, getPatchForVolumeReplaceStrategy(volName, obj, volToInsert.(map[string]interface{})))
		}
	}

	return patches
}

func getPatchForVolumeMergeStrategy(volToInsert map[string]interface{}) string {
	withFantic := map[string]map[string][]map[string]interface{}{"spec": {"volumes": {volToInsert}}}
	yamlPatch, err := yaml.Marshal(withFantic)
	if err != nil {
		return ""
	}
	return string(yamlPatch)
}

func getPatchForVolumeReplaceStrategy(volName string, obj, volToInsert map[string]interface{}) string {
	objVols, err := jmespath.Search("spec.volumes", obj)
	if err != nil || objVols == nil {
		return ""
	}

	objVolumes, ok := objVols.([]interface{})
	if !ok {
		return ""
	}

	var yamlPatch []byte

	for i := range objVolumes {
		if objVolumes[i].(map[string]interface{})["name"] != volName {
			continue
		}
		objVolumes[i] = volToInsert
		withFantic := map[string]map[string][]interface{}{"spec": {"volumes": objVolumes}}
		yamlPatch, err = yaml.Marshal(withFantic)
		if err != nil {
			return ""
		}

	}

	if len(yamlPatch) != 0 {
		return string(yamlPatch)
	}

	objVolumes = append(objVolumes, volToInsert)

	withFantic := map[string]map[string][]interface{}{"spec": {"volumes": objVolumes}}
	yamlPatch, err = yaml.Marshal(withFantic)
	if err != nil {
		return ""
	}
	return string(yamlPatch)
}

func getPatchForAnnotation(specData interface{}, annName string) []string {
	metadata, ok := specData.(map[string]interface{})["metadata"]
	if !ok {
		return nil
	}
	annots, ok := metadata.(map[string]interface{})["annotations"]
	if !ok {
		return nil
	}

	val, ok := annots.(map[string]interface{})[annName]
	if !ok {
		return nil
	}

	withFantic := map[string]map[string]map[string]interface{}{"metadata": {"annotations": {annName: val}}}
	yamlPatch, err := yaml.Marshal(withFantic)
	if err != nil {
		return nil
	}

	return []string{string(yamlPatch)}
}

func convert(i interface{}) interface{} {
	switch x := i.(type) {
	case map[interface{}]interface{}:
		m2 := map[string]interface{}{}
		for k, v := range x {
			m2[k.(string)] = convert(v)
		}
		return m2
	case []interface{}:
		for i, v := range x {
			x[i] = convert(v)
		}
	}
	return i
}
